package org.dreamwork.jasmine2.restful.client.gui.ui;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.dreamwork.gson.GsonHelper;
import org.dreamwork.jasmine2.restful.*;
import org.dreamwork.jasmine2.restful.client.gui.model.ParameterHolder;
import org.dreamwork.jasmine2.restful.client.gui.model.ParameterTableModel;
import org.dreamwork.jasmine2.restful.client.gui.util.Filters;
import org.dreamwork.jasmine2.restful.client.gui.util.HttpUtil;
import org.dreamwork.jasmine2.restful.client.gui.util.KeyUtil;
import org.dreamwork.jasmine2.restful.client.gui.util.ParameterUtil;
import org.dreamwork.misc.Base64;
import org.dreamwork.secure.SecureContext;
import org.dreamwork.secure.SecureUtil;
import org.dreamwork.util.IOUtil;
import org.dreamwork.util.StringUtil;

import java.io.*;
import java.security.PublicKey;
import java.text.SimpleDateFormat;
import java.util.*;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.URLEncoder;
import java.util.concurrent.ExecutionException;

/**
 * Created by IntelliJ IDEA.
 * User: seth.yang
 * Date: 2014/12/9
 * Time: 2:23
 */
public class MethodPanel {
    private JPanel root;
    private JLabel txtURI;
    private JLabel txtMethod;
    private JLabel txtDescription;
    private JLabel txtTitle;
    private JButton btnCall;
    private JPanel parameters;
    private JTextPane txtResult;
    private JTextArea txtBody;
    private JCheckBox chkEncrypt;
    private ParameterTable contextTable;
    private JButton btnSave;
    private JButton btnLoad;

    private ParameterTableModel model;

    private APIDefinition api;
    private MethodDefinition md;

    private static JFileChooser chooser = new JFileChooser ();
    private static File conf = new File ("../conf/.parameter-location");

    static {
        try {
            loadLocation ();
            chooser.setFileFilter (Filters.SAVED_PARAMETER);
        } catch (IOException e) {
            //
        }
    }

    public MethodPanel (APIDefinition api, MethodDefinition md) {
        this.api = api;
        this.md = md;

        guiSetup ();
    }

    public MethodPanel fill () {
        String prefix = System.getProperty ("api-url-prefix");
        txtURI.setText (prefix + '/' + api.getPattern () + '/' + md.getPattern ());
        txtMethod.setText (md.getType ().name ());
        String desc = md.getDescription ();
        if (!StringUtil.isEmpty (desc)) {
            desc = desc.replace ("\n", "<br/>");
            desc = "<html>" + desc + "</html>";
            txtDescription.setText (desc);
        }
        txtTitle.setText (md.getTitle ());
        return this;
    }

    public JPanel getPanel () {
        return root;
    }

    private void guiSetup () {
        txtResult.setEditable (false);
        btnCall.addActionListener (new ActionListener () {
            @Override
            public void actionPerformed (ActionEvent e) {
                new Thread () {
                    @Override
                    public void run () {
                        try {
                            call ();
                        } catch (Exception ex) {
                            ex.printStackTrace ();
                        }
                    }
                }.start ();
            }
        });

//        ButtonGroup group = new ButtonGroup ();

        if (md.getType () == MethodType.GET) {
            createTable ();
        } else {
            createTextBody ();
        }

        if (md.isForceEncrypt ()) {
            chkEncrypt.setSelected (true);
            chkEncrypt.setEnabled (false);
        }

        btnSave.addActionListener (new ActionListener () {
            @Override
            public void actionPerformed (ActionEvent e) {
                saveParameter ();
            }
        });
        btnLoad.addActionListener (new ActionListener () {
            @Override
            public void actionPerformed (ActionEvent e) {
                loadParameter ();
            }
        });
    }

    private void call () throws Exception {
        String appId = System.getProperty ("app-id");
        txtResult.setText ("");
        if (StringUtil.isEmpty (appId)) {
            throw new RuntimeException ("Missing App ID");
        }
        if (md.getType () == MethodType.GET) {
//            if (radioDispersed.isSelected ()) {
                if (model != null) {
                    if (!chkEncrypt.isSelected ())
                        callPlainGet (appId);
                    else
                        callEncryptedGet (appId);
                }
/*
            } else {
                if (txtBody != null && !StringUtil.isEmpty (txtBody.getText ())) {
                    if (!chkEncrypt.isSelected ())
                        callBodyGet (appId);
                    else
                        callBodyEncryptedGet (appId);
                }
            }
*/
        } else if (md.getType () == MethodType.POST) {
            callPost (appId, chkEncrypt.isSelected ());
        }
    }

    private String cast (Object value) {
        if (value == null) return "";
        if (value instanceof Date) {
            return new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss").format (value);
        }
        return value.toString ();
    }

    private void createTable () {
        ParameterTable table = new ParameterTable (api, md);
        model = (ParameterTableModel) table.getModel ();
        parameters.removeAll ();
        parameters.add (new JScrollPane (table));
    }

    private void createTextBody () {
        parameters.removeAll ();
        model = null;
        txtBody = new JTextArea ();
        txtBody.setRows (10);
        JScrollPane jsp = new JScrollPane (txtBody);
        parameters.add (jsp);
    }

    private StringBuilder buildGetParameters () throws UnsupportedEncodingException {
        Map<String, ParameterHolder> holders = model.getParameters ();
        StringBuilder builder = new StringBuilder ();
        for (String name : holders.keySet ()) {
            if (builder.length () > 0) builder.append ('&');
            ParameterHolder holder = holders.get (name);
            Object value = holder.getValue ();
            String e = cast (value);
            builder.append (name).append ('=').append (URLEncoder.encode (e, "utf-8"));
        }
        return builder;
    }

    private void callPlainGet (String appId) throws ExecutionException, InterruptedException, UnsupportedEncodingException {
        String target = getBaseURL (appId) + '&' + buildGetParameters ();
        String result = HttpUtil.get (target);
        setResultText (result);
    }

    private void callEncryptedGet (String appId) throws Exception {
        if (KeyUtil.getPublicKey () == null) {
            throw new RuntimeException ("Missing public key");
        }

        String baseURL = getBaseURL (appId);
        Map<String, ParameterHolder> holders = model.getParameters ();
        if (holders.size () > 1) {
            StringBuilder builder = buildGetParameters ();
            baseURL += "&encrypted_body=" + URLEncoder.encode (encrypt (builder), "utf-8");
        } else {
            String name = holders.keySet ().iterator ().next ();
            ParameterHolder holder = holders.get (name);
            Object value = holder.getValue ();
            if (value != null) {
                baseURL += "&" + name + "=" + URLEncoder.encode (encrypt (cast (value)), "utf-8");
            }
        }

        String content = HttpUtil.get (baseURL);
        Gson g = GsonHelper.getGson (true);
        Map<String, Object> map = g.fromJson (content, new TypeToken<Map<String, Object>> () {}.getType ());
        Number code = (Number) map.get ("code");
        if (code.intValue () != 0) {
            setResultText (content);
        } else {
            String encrypted_body = (String) map.get ("encrypted_body");
            String result = decrypt (encrypted_body);
            setResultText (result);
        }
    }

/*
    private void callBodyGet (String appId) throws UnsupportedEncodingException, ExecutionException, InterruptedException {
        String base = getBaseURL (appId);
        if (txtBody == null || StringUtil.isEmpty (txtBody.getText ())) {
            JOptionPane.showMessageDialog (root, "Missing required body");
            throw new IllegalArgumentException ("missing required body");
        }
        String text = txtBody.getText ().trim ();
        StringBuilder builder = new StringBuilder ();
        for (String pair : text.split ("&")) {
            if (!StringUtil.isEmpty (pair)) {
                String[] a = pair.split ("=");
                if (builder.length () > 0) builder.append ('&');
                builder.append (a[0]).append ('=').append (URLEncoder.encode (a[1].trim (), "utf-8"));
            }
        }
        String result = HttpUtil.get (base + '&' + builder);
        setResultText (result);
    }

    private void callBodyEncryptedGet (String appId) throws Exception {
        String base = getBaseURL (appId);
        String text = txtBody.getText ().trim ();
        if (md.getParameters ().size () > 1)
            base += "&encrypted_body=" + encrypt (text);
        else if (md.getParameters ().size () == 1) {
            String name = md.getParameters ().get (0).getName ();
            base += "&" + name + '=' + encrypt (text);
        }
        setResultText (decrypt (HttpUtil.get (base)));
    }
*/

    private void callPost (String appId, boolean encrypted) throws Exception {
        String url = getBaseURL (appId);
        String body = txtBody.getText ().trim ();
        if (encrypted)
            body = encrypt (body);
        String result = HttpUtil.post (url, body);
        if (encrypted) {
            TypeToken<Map<String, Object>> token = new TypeToken<Map<String, Object>> () {};
            Gson g = GsonHelper.getGson (true);
            Map<String, Object> map = g.fromJson (result, token.getType ());
            Number number = (Number) map.get ("code");
            if (number.intValue () == 0) {
                result = (String) map.get ("encrypted_body");
                result = decrypt (result);
            }
        }
        setResultText (result);
    }

    private void createUIComponents () {
        contextTable = new ParameterTable (api, md, true);
    }

    private String getBaseURL (String appId) throws UnsupportedEncodingException {
        ParameterTableModel model = (ParameterTableModel) contextTable.getModel ();
        Map<String, ParameterHolder> holders = model.getParameters ();
        StringBuilder builder = new StringBuilder ("appId=").append (appId);
        for (String name : holders.keySet ()) {
            ParameterHolder holder = holders.get (name);
            Object value = holder.getValue ();
            String e = cast (value);
            if (StringUtil.isEmpty (e)) {
                if (holder.isRequire ()) {
                    JOptionPane.showMessageDialog (root, "Missing required context parameter: " + name);
                    throw new IllegalArgumentException (name);
                }
            } else {
                builder.append ('&').append (name).append ('=').append (URLEncoder.encode (cast (value), "utf-8"));
            }
        }
        if (chkEncrypt.isSelected ())
            builder.append ("&encrypted=true");

        return txtURI.getText () + '?' + builder;
    }

    private String encrypt (CharSequence content) throws Exception {
        PublicKey kek = KeyUtil.getPublicKey ();
        SecureContext context = KeyUtil.getContext ();

        SecureUtil util = new SecureUtil (context);
        byte[] body = util.encrypt (content.toString ().getBytes ("utf-8"), kek);
        body = Base64.encode (body);
        return new String (body, "utf-8");
    }

    private String decrypt (CharSequence content) throws Exception {
        PublicKey kek = KeyUtil.getPublicKey ();
        SecureContext context = KeyUtil.getContext ();
        SecureUtil util = new SecureUtil (context);

        byte[] body = Base64.decode (content.toString ().getBytes ("utf-8"));
        body = util.decrypt (body, kek);
        return new String (body, "utf-8");
    }

    private void saveParameter () {
        new Thread () {
            @Override
            public void run () {
                try {
                    File file = new File (chooser.getCurrentDirectory (), md.getName () + ".sdp");
                    chooser.setSelectedFile (file);
                    int flag = chooser.showSaveDialog (root);
                    if (flag == JFileChooser.APPROVE_OPTION) {
                        file = chooser.getSelectedFile ();
                        if (file != null) {
                            PrintWriter out = null;
                            FileOutputStream fos = new FileOutputStream (conf);
                            try {
                                ParameterUtil.save (
                                        file, chkEncrypt.isSelected (), md.getType (),
                                        (ParameterTableModel) contextTable.getModel (),
                                        model,
                                        txtBody == null ? null : txtBody.getText ()
                                );

                                out = new PrintWriter (fos, true);
                                out.println (chooser.getCurrentDirectory ().getCanonicalFile ());
                            } finally {
                                if (out != null) {
                                    out.flush ();
                                    out.close ();
                                }
                                fos.flush ();
                                fos.close ();
                            }
                        }
                    }
                } catch (Exception ex) {
                    ex.printStackTrace ();
                }
            }
        }.start ();
    }

    private void loadParameter () {
        new Thread () {
            @Override
            public void run () {
                int flag = chooser.showOpenDialog (root);
                if (flag == JFileChooser.APPROVE_OPTION) {
                    File file = chooser.getSelectedFile ();
                    if (file != null) {
                        try {
                            final Map<String, Object> map = ParameterUtil.load (file);
                            SwingUtilities.invokeLater (new Runnable () {
                                @Override
                                @SuppressWarnings ("unchecked")
                                public void run () {
                                    chkEncrypt.setSelected ((Boolean) map.get ("encrypted"));
                                    Map<String, ParameterHolder> holders = (Map<String, ParameterHolder>) map.get ("context");
                                    ((ParameterTableModel) contextTable.getModel ()).setData (holders);
                                    if (md.getType () == MethodType.GET) {
                                        model.setData ((Map<String, ParameterHolder>) map.get ("parameters"));
                                    } else {
                                        txtBody.setText ((String) map.get ("body"));
                                    }
                                }
                            });
                        } catch (Exception ex) {
                            ex.printStackTrace ();
                        }
                    }
                }
            }
        }.start ();
    }

    private void setResultText (final String text) {
        SwingUtilities.invokeLater (new Runnable () {
            @Override
            public void run () {
                txtResult.setText (text);
            }
        });
    }

    private static void loadLocation () throws IOException {

        if (conf.exists ()) {
            String location = new String (IOUtil.read (conf), "utf-8");
            chooser.setCurrentDirectory (new File (location));
        } else {
            File dir = new File ("../saved-parameters");
            if (!dir.exists () && !dir.mkdirs ())
                return;
            chooser.setCurrentDirectory (dir);
        }
    }
}